<?php

require_once(dirname(__FILE__).'/../../resourcepage_extension_push.php');
require_once('tracker_database.php');
require_once(dirname(__FILE__).'/../../../../local/smb_connection.php');
require_once(dirname(__FILE__).'/../../../../lib/filelib.php');
require_once(dirname(__FILE__).'/../../../../local/utils.php');

class resourcepage_etexts extends resourcepage_extension_push {
    // These are variables not constants so they can be changed for testing
    private $etextshare='//esaki/LTS-eText_archive';

    // Regexpts for matching filenames
    
    // ISBN, optional whitespace, 9 or 12-digit number plus checkdigit
    const MATCH_ISBN='/(?:ISBN|isbn|N|n)\s*([0-9]{9}(?:[0-9]{3})?[0-9X])[^0-9]/';
    // SUP or s, optional whitespace, 6-digit number
    const MATCH_SUP='/(?:SUP|s|sup)\s*([0-9]{6})[^0-9]/';
    // WEB or web, optional whitespace, 6-digit number
    const MATCH_WEB='/(?:WEB|web)\s*([0-9]{6})[^0-9]/';
    // _v(number)_, where the number is either <major version><minor single digit>
    // or <major version>-<minor version>
    const MATCH_VERSION1='/_v(?:([0-9]+)([0-9])|([0-9]+)-([0-9]+))_/';
    const MATCH_VERSION2='/_e([0-9]+)i([0-9]+)_/';
    // Valid files are pdf, doc, rtf only (at present)    
    const MATCH_VALIDFILES='/\.(pdf|doc|rtf)$/';
    
    public function get_placeholders() {
        return array('studentetexts','tutoretexts');
    }
    public function get_database_version() {
        return '2007022800';
    }
    
    public function get_placeholder_description($placeholder,$placeholderitemid) {  
        $phdata=$this->get_maintable_record($placeholderitemid);
        $coursepres=$phdata->coursepres;
        if(!$coursepres) {
            $coursepres='(none)';
        }
        return 
            get_string('placeholder_'.$placeholder.'_description',
                'resourcepage_'.$this->get_name(),$coursepres).' '.
            get_string($phdata->allmailings ? 'description_allmailings' : 
                'description_virtualmailing','resourcepage_'.$this->get_name());            
    }
    
    protected function before_add_instance(&$content,$placeholder,$courseid) {
        // Get course details
        if(!$course=get_record('course','id',$courseid,'','','','','idnumber')) {
            throw new Exception('Failed to obtain course record #'.$courseid);
        }
        $content->coursepres=$course->idnumber;
        $content->allmailings=1;
    }
    
    protected function before_update_instance(&$content,$isadd) {
        $content->coursepres=strtoupper($content->coursepres);
        
        $this->resourcepage->set_plugin_info($content->placeholderitemid,
            get_string('waitforcron','resourcepage_etexts'));
    }   
    
    public function get_cron_frequency() {
        global $CFG;
        $frequency = 4*60*60; // Cron me every 4 hours;
        // if config frequency is a number use it.
        if(isset($CFG->etexts_cron_frequency) && is_numeric($CFG->etexts_cron_frequency)){
            $frequency = $CFG->etexts_cron_frequency;
        }
        
        return $frequency;
    }    
    
    public function cron() {
        global $CFG;
        
        //#7770 Only run during preset hours. 1am-6am being the defaults
        $date = getdate();
        
        // Set the defaults
        $earliest_start = 1; //1am
        $latest_start = 6; //6am
        
        /*
         * We can't validate the values when they're set so we have to validate
         * them now
         */
        // If the $CFG->etexts_cron_window_start is a valid hour use it
        if(isset($CFG->etexts_cron_window_start) && is_numeric($CFG->etexts_cron_window_start)
            && $CFG->etexts_cron_window_start >=0 && $CFG->etexts_cron_window_start <=23){
            $earliest_start = $CFG->etexts_cron_window_start;
        }
        
        // If the $CFG->etexts_cron_window_end is a valid hour use it
        if(isset($CFG->etexts_cron_window_end) && is_numeric($CFG->etexts_cron_window_end)
            && $CFG->etexts_cron_window_end >=0 && $CFG->etexts_cron_window_end <=23){
            $latest_start = $CFG->etexts_cron_window_end;
        }
        
        if($earliest_start>$latest_start){// reset to defaults
        	$earliest_start = 1; //1am
            $latest_start = 6; //6am
        }
        
        if(!$date || $date['hours']<$earliest_start || $date['hours']>$latest_start){
        	return;
        }
        
        $this->process_placeholder('studentetexts',false);
        $this->process_placeholder('tutoretexts',true);
        
 		if (empty($CFG->systemwindowsuser)) {
            return;
        }
        try {
	          $smb=new smb_connection($this->etextshare,$CFG->systemwindowsuser,$CFG->systemwindowspassword,'OPEN-UNIVERSITY');
        	  $this->update_archive($smb);
            $this->update_actual_files($smb);
        } catch(Exception $e) {
            mtrace("\n\nEXCEPTION:\n".$e->getMessage());
            global $CFG;
            $targets=isset($CFG->etextmailtargets) ? $CFG->etextmailtargets : ''; 
            $usernames=preg_split('/[, ]+/',$targets);
            foreach($usernames as $username) {
                if($user=get_record('user','username',$username)) {
                    email_to_user($user,'','VLE: etext archive error',
"An error occurred while processing the etext archive at:
{$this->etextshare}

This error prevents proper processing of etexts. Some files and courses
may not be processed until the error is resolved.

Error details:
{$e->getMessage()}

You will probably receive a copy of this message about every four hours
until the problem is fixed.");
                } 
            }
            if(count($usernames)>0) {
                mtrace("\nEmailed warning to etext admins.\n");
            }
        }        
    }
    
    public function update_archive(&$smb) {
        mtrace("\n* Updating etext archive");
        
        // 1. Get the list of all files in the archive
        mtrace('  1. Listing files in archive... ','');
        $start=time();
        $files=$smb->recursive_dir();
        mtrace('done ('.(time()-$start).' seconds)');
        
        $this->update_archive_table($files);
    }
    
    /**
     * Regenerates the database table that holds information
     * about files found in the etext archive.
     * @param array &$files Files from SMB connection 
     */
    public function update_archive_table(&$files) {
        // 2. Update the database
        $adds=$deletes=$unchanged=0;
        
        // a. Load entire table into memory (because we're built like that)
        //    Associative array of publiccode/edition.impression -> array of archivepath 
        mtrace('  2a. Loading existing table... ','');
        $start=time();
        $rs=$this->do_sql("SELECT (publiccode || '/' || edition || '.' || impression) AS thing,archivepath FROM extprefix_archv");
        $existingdata=array();
        while(!$rs->EOF) {            
            if(array_key_exists($rs->fields['thing'],$existingdata)) {
                $existingdata[$rs->fields['thing']][]=$rs->fields['archivepath'];
            } else {
                $existingdata[$rs->fields['thing']]=array($rs->fields['archivepath']);
            }    
            $rs->MoveNext();        
        }
        unset($rs); // probably won't save memory but whatever
        mtrace('done ('.(time()-$start).' seconds)');
            
        // b. Scan archive results and work out difference
        mtrace('  2b. Applying new additions... ','');
        $start=time();
        $alreadyseen=array();
        $tw=new transaction_wrapper();
        $matches=array();
        foreach($files as $file) {
            // Check it's the type of file we are considering
            if(preg_match(self::MATCH_ISBN,$file->filename,$matches)) {
                if(strlen($matches[1])==13) {
                    $publiccode='N'.$matches[1];
                } else {
                    $publiccode='ISBN'.$matches[1];
                }
            } else if(preg_match(self::MATCH_SUP,$file->filename,$matches)) {
                $publiccode='SUP'.$matches[1];
            } else if(preg_match(self::MATCH_WEB,$file->filename,$matches)) {
                $publiccode='WEB'.$matches[1];
            } else {
                // Must match the ISBN pattern or the SUP pattern to be considered
                continue;
            }
            
            // Check it has a version, either old or new type
            if(!preg_match(self::MATCH_VERSION2,$file->filename,$matches)) {
                if(!preg_match(self::MATCH_VERSION1,$file->filename,$matches)) {
                    // Note that the Promises version allowed files without versions,
                    // but I have decided not to.
                    continue;
                }
            }
            if($matches[1]) {
                $major=(int)$matches[1];
                $minor=(int)$matches[2];
            } else {
                $major=(int)$matches[3];
                $minor=(int)$matches[4]; 
            }
            
            // Key and value...
            $key=$publiccode.'/'.$major.'.'.$minor;
            $archivepath=$file->parentfolder.$file->filename;
            
            // At the moment only support .pdf, .rtf, and .doc 
            // (this avoids a few invalid files)
            if(!preg_match(self::MATCH_VALIDFILES,$file->filename)) {
                continue;
            }

            // Consider only first item with each extension
            $ext=preg_replace('/^.*\./','',$file->filename);
            $alreadycheck=$key.'_'.$ext;
            if(array_key_exists($alreadycheck,$alreadyseen)) {
                continue;
            }            
            $alreadyseen[$alreadycheck]=true;
            
            // Present in existing data?
            if(array_key_exists($key,$existingdata) && ($innerkey=array_search($archivepath,$existingdata[$key]))!==FALSE) {
                // OK then, remove from existingdata so we don't leave it in the 
                // 'to be deleted' category at end
                unset($existingdata[$key][$innerkey]);
                $unchanged++;
            } else {
                // Work out target name on disk. Example:
                // moddata/resourcepage/etexts/ - root path
                // ISBN/ - public code prefix
                // 4X/ - last 2 characters of public code
                // existingfilename.pdf - filename part of name  
                $filepath='moddata/resourcepage/etexts/'.preg_replace('/[^A-Z].*$/','',$publiccode).'/'.
                  substr($publiccode,strlen($publiccode)-2,2).'/'.
                  $file->filename;
                  
                // Add entry
                $this->do_sql('INSERT INTO extprefix_archv(publiccode,edition,impression,archivepath,filepath) VALUES ('.
                    $this->c($publiccode).",$major,$minor,".$this->c($archivepath).','.
                    $this->c($filepath).')');
                $adds++;
            }            
        }        
        mtrace('done ('.(time()-$start).' seconds)');
        
        // c. Delete all the previously-existing data that wasn't found
        mtrace('  2c. Applying deletions... ','');        
        $start=time();
        foreach($existingdata as $key=>$values) {
            list($publiccode,$edition,$impression)=preg_split('/[\/.]/',$key);
            foreach($values as $archivepath) {
                $sql=('DELETE FROM extprefix_archv WHERE publiccode='.$this->c($publiccode).
                    ' AND edition='.$edition.' AND impression='.$impression.' AND archivepath='.
                    $this->c($archivepath));
                $this->do_sql($sql);
                $deletes++; 
            }
        } 
        mtrace('done ('.(time()-$start).' seconds)');

        // OK, should now be up to date!            
        $tw->commit();
        mtrace("  Complete ($adds added, $deletes deleted, $unchanged unchanged)");        
    }
    
    /** 
     * Handles one or other of the placeholders for all courses.
     * @param string $phname Placeholder ID name
     * @param bool $tutors True if it's the tutor placeholder
     */ 
    private function process_placeholder($phname,$tutors) {
        // Find all the ebooks placeholders
        $placeholders=$this->resourcepage->get_placeholders($phname);
        
        // If there aren't any, do nothing
        if(count($placeholders)==0) {
            return;
        }
        
        $tracker=new tracker_database();
        
        // Process each course
        foreach($placeholders as $placeholderdetails) {
            
            $phdata=$this->get_maintable_record($placeholderdetails->placeholderitemid);
            if(!$phdata) {
                mtrace("\n* Warning: unprocessed placeholder {$placeholderdetails->placeholderitemid}");
                continue;
            }
            $tw=new transaction_wrapper();
            
            // Get course ID field and check if it's a real course...
            $course=get_record('course','id',$placeholderdetails->courseid);
            $coursepres=$phdata->coursepres;
            $matches=array();
            if($matches = get_course_code_pres($coursepres)) {
                list($junk,$coursecode,$prescode)=$matches;
                
                // Houston, we have a course code. Let's run the DB query...
                $items=$tracker->get_items($coursecode,$prescode,!$phdata->allmailings);
                
                // ...and now compare with existing items to see if we're missing anything
                foreach($items as $item) {
                    
                    // Stage 1: Check if we have the item on record in our global items store
                    //////////
                    
                    // Do we have an actual base item for that?
                    $rs=$this->do_sql(<<<ENDQUERY
SELECT 
    i.id,i.title
FROM 
    extprefix_items ei
    INNER JOIN modprefix_items i ON ei.itemid=i.id  
WHERE
    publiccode={$this->c($item->get_public_code())} AND
    edition={$item->get_edition_num()}
ENDQUERY
);
                    // Text to display by item
                    $infotext=$item->get_public_code().'/'.$item->get_edition_num();                             
                    
                    if($rs->EOF) {
                        // Item hasn't been added yet. Let's make a new happy item!
                        $baseitem=$this->resourcepage->add_item('file',false,null,null,null,
                            $item->get_title(),null,$infotext,null,null,null,null,null,0);
                        if(!preg_match('/^WEB/',$item->get_public_code())) {
                            $this->resourcepage->add_item_flag($baseitem,'printeditem');
                        }
                        $this->do_sql(
                            'INSERT INTO extprefix_items(itemid,publiccode,edition) VALUES ('.
                            $baseitem.','.$this->c($item->get_public_code()).','.$item->get_edition_num().')');
                    } else {
                        $baseitem=$rs->fields['id'];
                        // Has item changed?
                        if($rs->fields['title']!=$item->get_title())
                        {
                            $this->do_sql(
                                'UPDATE modprefix_items SET title='.$this->c($item->get_title()).
                                'WHERE id='.$baseitem);
                        }
                    }
                    
                    // Stage 2: Add an alias for baseitem to this specific placeholder if appropriate
                    //////////
                    
                    // Is it OK for intended audience?
                    if($tutors) {
                        // Tutor placeholder gets ONLY the tutor items, not those for tutors+students,
                        // because tutors can also access the student pages.
                        if($item->is_for_students() || !$item->is_for_tutors()) {
                            continue;
                        }
                    } else {
                        if(!$item->is_for_students()) {
                            continue;
                        }
                    }
                        
                    // Get the list of existing items added to placeholder
                    $existingpc=$this->resourcepage->get_placeholder_added_items($placeholderdetails->placeholderitemid);
                    
                    if(!in_array($baseitem,$existingpc)) {
                        // Item wasn't added yet, so add it now
                        $aliasid=$this->resourcepage->add_placeholder_item($placeholderdetails->placeholderitemid,$baseitem);
                        if(!$phdata->allmailings) {
                            $this->do_sql(
                                'UPDATE modprefix_items SET rights=1 WHERE id='.$aliasid);                            
                        }                        
                    }
                }
                $a=new stdClass;
                $a->items=count($items);
                $a->date=date(self::DATEFORMAT,time());
                $msg=get_string('lastchecked','resourcepage_etexts',$a);
                if($placeholderdetails->plugininfo!=$msg) {
                    $this->resourcepage->set_plugin_info($placeholderdetails->placeholderitemid,$msg);
                }
            } else {
                // Check placeholder has the 'no use putting this here' message.
                if(!$coursepres) {
                    $coursepres='(none)';
                }
                $msg=get_string('notoucourse','resourcepage_etexts',$coursepres);
                if($placeholderdetails->plugininfo!=$msg) {
                    $this->resourcepage->set_plugin_info($placeholderdetails->placeholderitemid,$msg);
                }
            }
            $tw->commit();
        }

    }

    const DATEFORMAT='j M Y H:i';
       
    public function update_actual_files(&$smb) {
        mtrace("\n* Updating etext files");
        mtrace('  1. Finding items with new impressions...','');
        $deleted=array();
        $start=time();
        // 1. Find all items where the max impression is bigger
        //    than the currently-stored one
        $query=<<<ENDQUERY
SELECT 
    xi.itemid,xi.currentimpression,MAX(xa.impression) AS maximpression
FROM 
    extprefix_items xi
    LEFT OUTER JOIN extprefix_archv xa 
        ON xa.publiccode=xi.publiccode 
        AND xa.edition=xi.edition
GROUP BY 
    xi.itemid,xi.currentimpression,xi.publiccode,xi.edition
HAVING
    (xi.currentimpression IS NULL OR MAX(xa.impression) > xi.currentimpression)
    AND MAX(xa.impression) IS NOT NULL
ENDQUERY;
        $rs=$this->do_sql($query);        
        if(!$rs->EOF) {
            mtrace('deleting outdated...');
        }
        while(!$rs->EOF) {
            
            mtrace('     Deleting files for item #'.$rs->fields['itemid'].', impression '.
                $rs->fields['currentimpression'].'->'.$rs->fields['maximpression']);
                
            // Find out extensions we're going to delete so we know later
            $rsdelete=$this->do_sql('SELECT filepath FROM modprefix_files WHERE itemid='.$rs->fields['itemid']);
            while(!$rsdelete->EOF) {
                $goneextension=preg_replace('/^.*\.([^.]*)$/','$1',$rsdelete->fields['filepath']);
                $deleted[$rs->fields['itemid']][$goneextension]=true;
                $rsdelete->MoveNext();
            }
            
            // Delete existing file entries (note: this won't delete user-added files
            // because those will have been added to the new item, not the base)
            $this->do_sql('DELETE FROM modprefix_files WHERE itemid='.$rs->fields['itemid']);
            
            // Update the new currentimpression value
            $this->do_sql('UPDATE extprefix_items SET currentimpression='.$rs->fields['maximpression'].
                ' WHERE itemid='.$rs->fields['itemid']);
            
            $rs->MoveNext();
        }
        mtrace('  done ('.(time()-$start).' seconds)');
        
        // 2. Find all items where there are files in the archive that aren't there as file
        //    entries yet.
        //    This works by finding all the archive entries that correspond (public code/edition/impression)
        //    to each etext item, along with the _files entries that correspond to that archive
        //    entry. Where there isn't a corresponding file item, obviously we need to add one.
        //    Piece of cake.
        mtrace('  2. Finding new unassigned files...','');
        $start=time();
        $query=<<<ENDQUERY
SELECT 
    xi.itemid,xa.archivepath,xa.filepath,fl.flag
FROM 
    extprefix_items xi
    INNER JOIN extprefix_archv xa 
        ON xi.publiccode=xa.publiccode
        AND xi.edition=xa.edition
        AND xi.currentimpression=xa.impression
    LEFT OUTER JOIN modprefix_files f 
        ON f.itemid=xi.itemid 
        AND f.filepath=xa.filepath
    LEFT OUTER JOIN modprefix_itemflags fl
        ON fl.itemid=xi.itemid
        AND fl.flag='inaccessible'
WHERE
    f.filepath IS NULL   
ENDQUERY;
        $rs=$this->do_sql($query);
        if(!$rs->EOF) {
            mtrace('adding new...');
        }
        $lastid=0; $accessible=array(); 
        while(!$rs->EOF) {
            
            // Check for physical file, copy if needed (it should be, 
            // really!)
            global $CFG;
            $fullpath=$CFG->dataroot.'/'.$rs->fields['filepath'];
            if($existed=file_exists($fullpath)) {
                unlink($fullpath);
            }
            mkdir_recursive(dirname($fullpath));
            $smb->get(preg_replace('/^\//','',$rs->fields['archivepath']),$fullpath);

            // Get basic details
            $extension=preg_replace('/^.*\.([^.]*)$/','$1',$rs->fields['filepath']);
            $itemid=$rs->fields['itemid'];
            
            // If item had 0 files...
            $query=<<<ENDQUERY
SELECT 
    COUNT(*) AS cnt
FROM 
    modprefix_files 
WHERE 
    itemid=$itemid AND 
    filepath LIKE '%.$extension'
ENDQUERY;
            $rs2=$this->do_sql($query);
            if(!isset($deleted[$itemid][$extension]) && $rs2->fields['cnt']==0) {
                // Delete any files with same extension from based-on items. This
                // is done because sometimes SDT add files before the automatic
                // one is available, which ought to be overwritten.
                $query=<<<ENDQUERY
DELETE FROM 
    modprefix_files 
WHERE 
    itemid IN (SELECT id FROM modprefix_items WHERE basedonitemid IS NOT NULL AND basedonitemid=$itemid) AND 
    filepath LIKE '%.$extension'
ENDQUERY;
                $this->do_sql($query);                
            }
            
            // Delete any existing files with same extension (we only support
            // one file of each type per item)
            $query=<<<ENDQUERY
DELETE FROM 
    modprefix_files 
WHERE 
    itemid=$itemid AND 
    filepath IN (
        SELECT 
            f2.filepath 
        FROM 
            modprefix_files f2 
        WHERE 
            f2.itemid=itemid AND
            f2.filepath LIKE '%.$extension'
    ) 
ENDQUERY;
            $this->do_sql($query);
            
            // Add file item
            $this->do_sql('INSERT INTO modprefix_files(itemid,filepath,mimetype,"size") VALUES ('.
                $rs->fields['itemid'].','.
                $this->c($rs->fields['filepath']).','.
                $this->c(mimeinfo('type',$rs->fields['filepath'])).','.
                filesize($fullpath).')');
                
            // If this is a new item, forget any updates to accessible flag
            if($rs->fields['itemid']!=$lastid) {  
                $lastid=$rs->fields['itemid'];
                $accessible[$lastid]=new stdClass;
                $accessible[$lastid]->current=is_null($rs->fields['flag']);
                $accessible[$lastid]->desired=false;
            }
            $accessible[$lastid]->desired |=
                preg_match('/_[Ll]3\.pdf$/',$rs->fields['filepath']); 
            
            mtrace('     Added file item for #'.$rs->fields['itemid'].': '.$rs->fields['filepath'].
                ($existed?' [Overwrote existing]':' [New]')); 
            
            $rs->MoveNext();
        }
        
        foreach($accessible as $itemid=>$accessible) {
            if($accessible->desired!=$accessible->current) {
                if($accessible->desired) { 
                    $this->resourcepage->delete_item_flag($itemid,'inaccessible');
                } else {
                    $this->resourcepage->add_item_flag($itemid,'inaccessible');
                }
            }
        }
        
        mtrace('  done ('.(time()-$start).' seconds)');
    }
}

?>